高级定时器
# 高级定时器
[TOC]
# 一、重复的定时器
- 一般情况下setTimeout用于延迟执行某方法或功能;setInterval则一般用于刷新表单,对于一些表单的假实时指定时间刷新同步。
# 1.1 setInterval的问题
当使用setInterval()时,仅当没有该定时器的任何其他代码实例时,JS引擎才将定时器代码添加到队列,确保了定时器代码加入队列中的最小时间间隔为指定间隔。
但仍然会存在两个问题(详见高程611页):
- 某些间隔会被跳过。
- 多个定时器的代码执行之间的间隔可能会比预期的小。
# 1.2 解决:链式setTimerout()调用
setTimeout(function) {
// 处理中...
// 使用arguments.callee来获取当前执行的函数的引用
setTimeout(arguments.callee, interval);
}, interval);
1
2
3
4
5
6
7
2
3
4
5
6
7
- 在前一个定时器代码执行之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。
- 可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。
# 二、数组分块
- 运行在浏览器中的JavaScript都被分配了一个数量的资源。
- 如果代码循行超过特定的时间或者特定语句数量就不让它继续执行。
- 脚本长时间运行的问题通常是有两个原因造成的:
- 过长的、过深的嵌套函数调用
- 进行大量处理的循环
# 2.1 大量处理的循环
for (var i=0, len=data.length; i < len; i++ ){
process(data[i]);
}
1
2
3
2
3
由于JavaScript的执行是一个阻塞操作,脚本运行所花时间越久,用户无法与页面交互的时间也越久。
# 2.2 数组分块
当该处理不是必须同步完成,数据不是必须按顺序完成,就可以用定时器分割这个循环。
这是一种叫做数组分块的技术,小块小块地处理数组,通常每次一小块。
# 2.2.1 基本思路
为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。
# 2.2.2 实现
// 处理的项目的数组,用于处理项目的函数,可选的运行该函数的上下文
function chunk(array, process, context) {
setTimeout(function() {
var item = array.shift();
process.call(context, item);
if(array.length > 0) {
setTimeout(arguments.callee, 100);
}
}, 100);
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
- 重要性:将多个项目的处理在执行队列上分开,在每个项目处理之后,给予其他的浏览器处理机会运行,这样就可以避免长时间运行脚本错误。
- 一旦某个函数需要花50ms以上的时间完成,那么最好看看能否将任务分割为一系列可以使用定时器的小任务。
# 三、节流与防抖
DOM操作比起非DOM交互需要更多的内存和CPU时间。连续尝试进行过多的DOM相关操作可能会导致浏览器挂起,有时候甚至会崩溃。
参考链接::https://juejin.im/post/5cbc68095188251ae95d3f4a
# 3.1 函数节流
节流:在指定的时间间隔内只会执行一次任务。
触发高频事件后,n秒后函数才会执行一次,如果n秒内高频事件再次被触发,则无视该触发,等事件执行完后,才重新触发。
某些代码不可以在没有间断的情况连续重复执行,只有在执行函数的请求停止了一段时间后才执行。
可用来处理scroll和resize事件。
function throttle(fn, interval, params) {
let last = 0;
return () => {
let now = +new Date();
if (now - last >= interval) {
last = now;
fn(...params);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 3.2 函数防抖
防抖:任务触发的间隔超过指定的间隔才会被执行。
触发高频事件后,n秒后函数才会执行一次,如果n秒内高频事件再次被触发,则重新等待n秒。
function debounce(fn, delay, params) {
let timer = null;
return () => {
if(timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(null, params);
}, delay);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3.3 节流与防抖
防抖和节流可视化比较:http://demo.nimius.net/debounce_throttle/
- 在高频触发事件中
- 节流就是第一个触发的事件说的算。当第一次触发事件,就开始计时,等待一定时间后执行,并且在这等待的时间再次触发的事件都会被屏蔽掉。
- 防抖则是最后一个说的算。每次触发事件都会清除之前的定时器,再新建一个。
# 3.4 优化首屏体验-懒加载
// 获取所有的图片标签
const imgs = document.getElementsByTagName('img')
// 获取可视区域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight
// num用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
let num = 0
function lazyload(){
for(let i=num; i<imgs.length; i++) {
// 用可视区域高度减去元素顶部距离可视区域顶部的高度
let distance = viewHeight - imgs[i].getBoundingClientRect().top
// 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
if(distance >= 0 ){
// 给元素写入真实的src,展示图片
imgs[i].src = imgs[i].getAttribute('data-src')
// 前i张图片已经加载完毕,下次从第i+1张开始检查是否露出
num = i + 1
}
}
}
// 监听Scroll事件
window.addEventListener('scroll', lazyload, false);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3.4.1 用Throttle来优化Debounce
// fn是我们需要包装的事件回调, delay是时间间隔的阈值
function throttle(fn, delay) {
// last为上一次触发回调的时间, timer是定时器
let last = 0, timer = null
// 将throttle处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this
// 保留调用时传入的参数
let args = arguments
// 记录本次触发回调的时间
let now = +new Date()
// 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
if (now - last < delay) {
// 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
clearTimeout(timer)
timer = setTimeout(function () {
last = now
fn.apply(context, args)
}, delay)
} else {
// 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
last = now
fn.apply(context, args)
}
}
}
// 用新的throttle包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34